package com.googlecode.jawb;

import java.io.IOException;
import java.io.InputStream;

import com.googlecode.jawb.clues.SetType;
import com.googlecode.jawb.clues.types.DynamicLengthType;
import com.googlecode.jawb.clues.types.StaticLengthType;

/**
 * Specifies all possible binary types (do not misplace with Java primitive
 * types).
 */
public enum BinaryType {
	/**
	 * Not a primitive. Load an object instead (with the same type as a field).
	 * Also use this value for any type which should be loaded using custom
	 * converter.
	 * 
	 * @see StaticLengthType
	 * @see DynamicLengthType
	 * @see SetType#dynamicLengthType()
	 * @see SetType#staticLengthType()
	 */
	OBJECT {
		/**
		 * @throws UnsupportedOperationException
		 *             Always throws this exception. Object cannot be loaded as
		 *             a long.
		 */
		@Override
		public long loadNumericValue(InputStream stream) {
			throw new UnsupportedOperationException();
		}

		/**
		 * @return Always returns null. Object cannot be loaded as an array of
		 *         bytes.
		 */
		@Override
		public byte[] toByteArray(long value) {
			return null;
		}
	},

	/**
	 * 8-bit unsigned integer number.
	 */
	UINT_8 {
		@Override
		public long loadNumericValue(InputStream stream) throws IOException {
			return nextByte(stream);
		}

		@Override
		public byte[] toByteArray(long value) {
			return Type.BYTE.toByteArray(value);
		}
	},
	
	/** 16-bit unsigned integer number. */
	UINT_16 {
		@Override
		public long loadNumericValue(InputStream stream) throws IOException {
			return (nextByte(stream) << 8) + nextByte(stream);
		}

		@Override
		public byte[] toByteArray(long value) {
			return Type.SHORT.toByteArray(value);
		}
	},
	
	/** 32-bit unsigned integer number. */
	UINT_32 {
		@Override
		public long loadNumericValue(InputStream stream) throws IOException {
			return (nextByte(stream) << 24) + (nextByte(stream) << 16) + (nextByte(stream) << 8) + nextByte(stream);
		}

		@Override
		public byte[] toByteArray(long value) {
			return Type.INT.toByteArray(value);
		}
	};

	/**
	 * Loads next byte from the input stream.
	 * 
	 * @param stream
	 * @return Loaded unsigned byte.
	 * @throws IOException
	 *             I/O error. Thrown by InputStream.
	 * @throws RuntimeException
	 *             If end of stream is reached and no data was read.
	 */
	private static int nextByte(InputStream stream) throws IOException {
		int value = stream.read();
		
		if (value == -1) {
			throw new RuntimeException("End of stream is reached, but loading has not been completed.");
		}
		
		return value;
	}
	
	/**
	 * Loads value and returns a result as a long.
	 * 
	 * @param stream
	 *            Stream from which a value will be loaded.
	 * @return Long representing a value.
	 */
	public abstract long    loadNumericValue(InputStream stream) throws IOException;
	
	/**
	 * @param value
	 *            Number to be converted.
	 * @return Array of bytes representing a given value.
	 */
	public abstract byte[]  toByteArray(long value);
}
